/*
 * Copyright 2012 SerGenis. All rights reserved.
 */
package com.sergenis.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.util.Queue;

/**
 * @author SerGenis
 */
public class SerialClone {

	public static <T> T clone(T object) {
		try {
			return cloneX(object);
		} catch (IOException e) {
			throw new IllegalArgumentException(e);
		} catch (ClassNotFoundException e) {
			throw new IllegalArgumentException(e);
		}
	}

	private static <T> T cloneX(T x) throws IOException, ClassNotFoundException {
		ByteArrayOutputStream baouts = new ByteArrayOutputStream();
		CloneOutput cout = new CloneOutput(baouts);
		cout.writeObject(x);
		byte[] bytes = baouts.toByteArray();

		ByteArrayInputStream bains = new ByteArrayInputStream(bytes);
		CloneInput cin = new CloneInput(bains, cout);

		T clone = (T) cin.readObject();
		return clone;
	}

	private static final class CloneOutput
			extends ObjectOutputStream {

		Queue<Class<?>> classQueue = new java.util.LinkedList<Class<?>>();

		public CloneOutput(OutputStream out) throws IOException {
			super(out);
		}

		@Override
		protected void annotateClass(Class<?> cl) throws IOException {
			classQueue.add(cl);
		}

		@Override
		protected void annotateProxyClass(Class<?> cl) throws IOException {
			classQueue.add(cl);
		}
	}

	private static final class CloneInput
			extends ObjectInputStream {

		private final CloneOutput output;

		public CloneInput(InputStream in, CloneOutput output)
				throws IOException {
			super(in);
			this.output = output;
		}

		@Override
		protected Class<?> resolveClass(ObjectStreamClass desc)
				throws IOException, ClassNotFoundException {
			Class<?> c = output.classQueue.poll();
			String expected = desc.getName();
			String found = (c == null) ? null : c.getName();

			if (!expected.equals(found)) { throw new InvalidClassException(
					"Classes desynchronized: found '" + found
							+ "' when expecting '" + expected + "'."); }
			return c;
		}

		@Override
		protected Class<?> resolveProxyClass(String[] interfaces)
				throws IOException, ClassNotFoundException {
			return output.classQueue.poll();
		}
	}
}
